//************************************************************
//************************************************************
//  TPendulumView
//  Copyright (c) 2009 by Charith Amarasinghe
//  SOME RIGHTS RESERVED
//************************************************************
//************************************************************
// Date: 9/03/2009                Coded by: Charith Amarasinghe
// Filename: PendulumView.pas     Module name: TPendulumView
//                                Source file: PendulumView.pas 
// Program description:
// Delphi VCL component tailored to draw a simple pendulum on
// its canvas.
//
//************************************************************
// Libraries and software support:
// - Delphi 5 and higher
// - Requires TickUtils.pas
// - Requires Windows 2000 or higher (possible GDI limitations)
//
//************************************************************
// Development environment:
// - Borland Delphi 5
// - Windows 2003 Server SP3
//
//************************************************************
// System requirements:
// - Boarland Delphi 5 Compiler
// - Microsoft Windows
//************************************************************
// Start date: 9/03/2009
// Update history:
//            DATE            MODIFICATION
//			11/03/2009		  Added tick drawing capability
//          16/03/2009		  Code refactored
//							  Added angle visualizer
//							  Added arrow drawing
//************************************************************
// Test history:
//  TEST PROTOCOL           DATE       TEST RESULTS
//	Testing in GUI			Rolling
//
//************************************************************
// Programmer comments:
//
//
//************************************************************


unit PendulumView;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, Math, TickUtils, PendulumViewUtils, MathUtils;



type
  //TPendulumView subclasses a TPaintBox
  TPendulumView = class(TPaintBox)

  private
  
	_debug_en : boolean;
	
     //Pendulum Properties
     _angle : real; //Angle of pendulum in radians
     _stringLength: real; //length of string in metres
     _scale: real; //pixels per metre



     //Reference x, y co-ordinates
     _center : TPoint; //Center of PendulumView
     _origin : TPoint; //Origin of PendulumString

     //Appearance properties
     _backgroundColor: TColor; //Colour of background
     _bobColor: TColor; //Colour of pendulum bob
     _stringColor: TColor; //Colour of string (also used for pendulum bob border)
     _shortTickColor: TColor; //Colour of short ticks
     _longTickColor: TColor; //Colour of long ticks (also used for long tick text)
     _anglePointerColor : TColor;
	 
     _shortTickPenWidth: integer; //Width of pen used to draw short ticks
     _longTickPenWidth: integer; //Width of pen used to draw long ticks
	 _stringPenWidth: integer;
	 
	 //Angle Pointer
	 _anglePointerPenWidth : integer;
	 _anglePointerLength : integer;
	 
     //Tick Properties
     _shortTickLength: integer; //Length of short ticks in pixels
     _shortTickInterval: integer; //Short ticks drawn every _shortTickInterval degrees
     _longTickLength: integer; //Length of long ticks in pixels
     _longTickInterval: integer; //Long ticks drawn ever _longTickInterval degrees

     _tickDistance: integer; //Distance of tick starting point from origin

	 //Pendulum Bob Properties
     _bobDiameter : integer; //Diameter of pendulum bob in pixels
     
	 //Private Procedures
	 procedure _drawOriginYReference; //Handles drawing the vertical dashed line at the origin
     procedure _drawPendulumString; //Handles drawing the pendulum string
     procedure _drawPendulumBob; //Handles drawing the pendulum bob
     procedure _drawTicks; //Handles drawing pendulum ticks
     procedure _drawAngleArc; //Handles drawin the angle arc
     procedure _drawArrowHead(Point, StartPoint, EndPoint : TPoint; HeadLength, PenWidth : Integer; PenColor : TColor);
     procedure _drawAnglePointer;
     procedure _resetDrawingTools;
	 
	 procedure _drawAngleText(x, y: integer);
         function _getAngleTextSize : TSize;

     procedure _setAngle(angle: real);
     function _getAngle : real;
     //Private Utility Functions (Consider moving to seperate unit)
     function _getBobLocation(Angle, Length : Real; Origin : TPoint) : TPoint; //Returns bob location coords given an angle and string length 
     function _getCircleRect(Center : TPoint; Diameter : Real): TRect; //Returns a square centered at TPoint Center with width = diameter
     
  public
     constructor Create(AOwner: TComponent); override; //Override the constructer for initial settings
     procedure Paint; override; //Override the paint function to redraw our paintbox
	 property Origin : TPoint Read _origin Write _origin;
	 
  published
	//Expose public properties (visible in property editor)
     property Angle: Real Read _getAngle Write _setAngle;
     property StringLength: Real Read _stringlength Write _stringlength;
     property Scale: Real Read _scale Write _scale;

     property BackgroundColor: TColor Read _backgroundColor Write _backgroundColor;
     property BobColor : TColor Read _bobColor Write _bobColor;
     property StringColor : TColor Read _stringColor Write _stringColor;

     property BobDiameter : Integer Read _bobDiameter Write _bobDiameter;

     
	 //property OriginX : Integer Read _origin.x Write _origin.x;
	 property OriginY : Integer Read _origin.y Write _origin.y;
     property Center : TPoint Read _center;
	 
	 property AnglePointerColor : TColor Read _anglePointerColor Write _anglePointerColor;
	 property AnglePointerPenWidth : Integer Read _anglePointerPenWidth Write _anglePointerPenWidth;
	 property AnglePointerLength : Integer Read _anglePointerLength Write _anglePointerLength;
    
     property ShortTickColor: TColor Read _shortTickColor Write _shortTickColor;
     property ShortTickLength: Integer Read _shortTickLength Write _shortTickLength;
     property ShortTickInterval: Integer Read _shortTickInterval Write _shortTickInterval;
     property ShortTickPenWidth: Integer Read _shortTickPenWidth Write _shortTickPenwidth;
    
     property LongTickColor: TColor Read _LongTickColor Write _LongTickColor;
     property LongTickLength: Integer Read _LongTickLength Write _LongTickLength;
     property LongTickInterval: Integer Read _LongTickInterval Write _LongTickInterval;
     property LongTickPenWidth: Integer Read _LongTickPenWidth Write _LongTickPenwidth;
	 
	 property StringPenWidth: Integer Read _stringPenWidth Write _stringPenWidth;
    
     property TickDistance: integer Read _tickDistance Write _tickDistance;

  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Additional', [TPendulumView]);
end;



//Constructor
constructor TPendulumView.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  _angle := pi/4;
  _stringLength := 1;
  _scale := 200;

  _center.X := ClientRect.Right div 2;
  _center.Y := ClientRect.Bottom div 2;

  _origin.X := _center.X;
  _backgroundColor := clWhite;
  _bobColor := clBlack;
  _stringColor := clBlack;
  
  _shortTickLength := 5;
  _shortTickColor := clBlack;
  _shortTickPenWidth := 1;
  _shortTickInterval := 2;

  _longTickLength := 10;
  _longTickColor := clBlack;
  _longTickPenWidth := 2;
  _longTickInterval := 10;
  
  _anglePointerColor := clRed;
  _anglePointerLength := 20;
  _anglePointerPenWidth := 2;
  
  _tickDistance := 100;

  _bobDiameter := 20;

end;



procedure TPendulumView.Paint;
begin

  inherited Paint;
  
  _center.X := ClientRect.Right div 2;
  _center.Y := ClientRect.Bottom div 2;

  _origin.X := _center.X;

  Canvas.Brush.Color := BackgroundColor;
  Canvas.Brush.Style := bsSolid;
  Canvas.FillRect(ClientRect);

  _drawOriginYReference;




  _drawTicks;
  _drawAngleArc;
  _drawAnglePointer;
  _drawPendulumString;
  _drawPendulumBob;
  
  _drawAngleText(ClientRect.Right-_getAngleTextSize.cx - 10, ClientRect.Bottom-3*_getAngleTextSize.cy-10);

end;


//private functions

//Get Circle Rect
//---------------
//Center : Center of required circle
//Diameter : Real
//Returns TRect centered at center with width Diameter
function TPendulumView._getCircleRect(Center : TPoint; Diameter : Real): TRect;
begin
  _getCircleRect.Top := Center.Y - Round(Diameter/2);
  _getCircleRect.Left := Center.X - Round(Diameter/2);
  _getCircleRect.Bottom := Center.Y - Round(Diameter/2) + Round(Diameter);
  _getCircleRect.Right := Center.X - Round(Diameter/2) + Round(Diameter);
end;



//Get Bob Location
//-----------------
//Angle: angle from horizontal
//Length: length of string in pixels
//Origin: Origin of motion
//Returns x, y of bob

function TPendulumView._getBobLocation(Angle, Length : Real; Origin : TPoint) : TPoint;
begin
    _getBobLocation.X := Round(Length*Cos(Angle)) + Origin.X;
    _getBobLocation.Y := Round(Length*Sin(Angle)) + Origin.Y;
end;


//Reset Drawing Tools
procedure TPendulumView._resetDrawingTools;
begin
	//Pen
	Canvas.Pen.Color := _stringColor;
	Canvas.Pen.Style := psSolid;
	Canvas.Pen.Width := 1;
	
	//Brush
	Canvas.Brush.Color := _backgroundColor;
	Canvas.Brush.Style := bsSolid;
end;

//Draw Origin Y Reference
//-----------------

procedure TPendulumView._drawOriginYReference;
begin
	_resetDrawingTools;

    Canvas.Pen.Color := _longTickColor;
	Canvas.Pen.Style := psDashDot;
	Canvas.Pen.Width := 1;
	
	Canvas.MoveTo(_origin.X, _origin.Y);
	Canvas.LineTo(_origin.X, _origin.Y + round(_stringLength*_scale*0.80));
end;

//Draw Pendulum
//-----------------

procedure TPendulumView._drawPendulumBob;
var
    PendulumLocation : TPoint;
    CircleRect : TRect;
begin

	_resetDrawingTools;
	
    Canvas.Brush.Color := _bobColor;
    //Canvas.Brush.Style := bsSolid;
    Canvas.Pen.Color := _stringColor;
    
    PendulumLocation := _getBobLocation(_angle, _stringLength*_scale, _origin);
    Canvas.MoveTo ( PendulumLocation.X, PendulumLocation.Y);
    Canvas.Ellipse(_getCircleRect(PendulumLocation, _bobDiameter));

end;


//Draw Pendulum String
//------------------

procedure TPendulumView._drawPendulumString;
var
    PendulumLocation : TPoint;

begin
    _resetDrawingTools;
	
    Canvas.Pen.Color := _stringColor;
    Canvas.Pen.Width := _stringPenWidth;
    PendulumLocation := _getBobLocation(_angle, _stringLength*_scale, _origin);
    Canvas.MoveTo (PendulumLocation.X, PendulumLocation.Y);
    Canvas.LineTo (Origin.X, Origin.Y)
end;

//Draw Angle Arc
//---------------------
procedure TPendulumView._drawAngleArc;
var
    pendulumLocation, arcTerminate, arrowVectStart : TPoint;
    arcRect : TRect;
begin

	_resetDrawingTools;
	
    Canvas.Pen.Color := _longTickColor;
    Canvas.Brush.Style := bsClear;

    pendulumLocation := _getBobLocation(_angle, _stringLength*_scale, _origin);
    arcRect := _getCircleRect(_origin, _stringLength*_scale/3);

    //arcTerminate.x := _origin.x + round(_stringLength*_scale/6*cos(_angle));
    //arcTerminate.y := _origin.y + round(_stringLength*_scale/6*sin(_angle));

    //arrowVectStart.y := _origin.y+round(_stringLength*_scale/6/sin(_angle));

    if _angle < pi/2 then
    begin
    Canvas.Arc(arcRect.Left, arcRect.Top, arcRect.Right, arcRect.Bottom, _origin.X, _origin.Y + 10, pendulumLocation.x, pendulumLocation.y);
    //arrowVectStart.x := _origin.x;

    end
    else if _angle > pi/2 then
    begin
    Canvas.Arc(arcRect.Left, arcRect.Top, arcRect.Right, arcRect.Bottom, pendulumLocation.x, pendulumLocation.y,  _origin.X, _origin.Y + 10);
    //arrowVectStart.x := pendulumLocation.x + 100;
    end;
    //Canvas.TextOut(100,100, IntToStr(arrowVectStart.x)+' '+IntToStr(arrowVectStart.y));
    //_drawArrowHead(arcTerminate, arrowVectStart ,arcTerminate, 5,1,_longTickColor);
end;

//Draws a arrow head
//----------------------
//Point - Arrow head point will be drawn here
//StartPoint - Begining of direction vector
//EndPoint - End of direction vector
//HeadLength - Length of arrow head line thingies
//PenWidth - Width of pen used to draw arrow head
//PenColor - Color of penUsed to draw arrow head
procedure TPendulumView._drawArrowHead(Point, StartPoint, EndPoint : TPoint; HeadLength, PenWidth : Integer; PenColor : TColor);
var
	lineAngle: real;
	HeadTop, HeadBottom: TPoint;
begin
	_resetDrawingTools;
	
	//Begin by calculating the angle from the horizontal
	if (StartPoint.x = EndPoint.x) then lineAngle:= pi/2 else
	lineAngle := arctan((EndPoint.y-StartPoint.y)/(EndPoint.x-StartPoint.x));
	
	//Canvas.TextOut(100,100, rad2degRS(lineAngle));
	
	//I'm sticking to a 45 degree arrow head, this is a basic transformation which might not work
	if lineAngle >= 0 then
	begin
	HeadTop.x := Point.x-round(headLength*cos(lineAngle-pi/4));
	HeadTop.y := Point.y-round(headLength*sin(lineAngle-pi/4));

	HeadBottom.x := Point.x-round(headLength*cos(lineAngle+pi/4));
	HeadBottom.y := Point.y-round(headLength*sin(lineAngle+pi/4));
	end
	else
	begin
	HeadTop.x := Point.x+round(headLength*cos(lineAngle-pi/4));
	HeadTop.y := Point.y+round(headLength*sin(lineAngle-pi/4));

	HeadBottom.x := Point.x+round(headLength*cos(lineAngle+pi/4));
	HeadBottom.y := Point.y+round(headLength*sin(lineAngle+pi/4));
	end;
	
	//Drawing bits
	Canvas.Pen.Width := PenWidth;
	Canvas.Pen.Color := PenColor;
	
	Canvas.MoveTo(Point.x, Point.y);
	Canvas.LineTo(HeadTop.x, HeadTop.y);
	
	Canvas.MoveTo(Point.x, Point.y);
	Canvas.LineTo(HeadBottom.x, HeadBottom.y);
	
	if _debug_en then
	begin
	Canvas.MoveTo(StartPoint.x, StartPoint.y);
	Canvas.LineTo(EndPoint.x, EndPoint.y)
	end;
	
end;


//Draws major ticks and minor ticks
//----------------------------------
procedure TPendulumView._drawTicks;
var
	currentAngle : real;	
	startP, endP : TPoint;
begin
	
	_resetDrawingTools;
		
	//Set the pen styles for short ticks
	Canvas.Pen.Width := _shortTickPenWidth; 
	Canvas.Pen.Color := _shortTickColor;
	currentAngle := 0; //reset currentAngle to 0
	
	Canvas.Brush.Color := _backgroundColor; //This is for the text background
	
	//Start with short ticks every tick interval from 0 to pi (180 degrees)
	while currentAngle <= pi*1.01 do 
	//dirty hack makes sure we stop slightly beyond 180degs; this is because currentAngle might skip 180degs
	begin
		startP := GetTickStartPoint(_origin, currentAngle, _tickDistance); //Find the starting point for our tick
		endP := GetTickEndPoint(_origin, currentAngle, _shortTickLength, _tickDistance); //Find the ending point for our tick
		Canvas.MoveTo(startP.X, startP.Y); //Move pen to start point
		Canvas.LineTo(endP.X, endP.Y); //Draw line to end point
	
		currentAngle := currentAngle + deg2rad(_shortTickInterval); //increment currentAngle
	end;
	
	//Set the pen styles for long ticks
	Canvas.Pen.Width := _longTickPenWidth;
	Canvas.Pen.Color := _longTickColor;
	currentAngle := 0; //reset currentAngle to 0
	
	while currentAngle <= pi*1.01 do
	//Like we did above
	begin
		startP := GetTickStartPoint(_origin, currentAngle, _tickDistance);
		endP := GetTickEndPoint(_origin, currentAngle, _shortTickLength, _tickDistance);
		Canvas.MoveTo(startP.X, startP.Y);
		Canvas.LineTo(endP.X, endP.Y);
		
		//Following logic writes out the angle at each long tick
		//TODO: REFACTOR
		//Refactor complete 16/03: however code could be made more elegant
	    if currentAngle < deg2rad(85) then
	        Canvas.TextOut(EndP.X+4, EndP.Y+3, rad2degRS(pi/2-currentAngle))
	    else if (currentAngle >- deg2rad(85)) and (currentAngle <= deg2rad(95)) then
	        Canvas.TextOut(EndP.X-(Canvas.TextWidth(rad2degRS(currentAngle)) div 2), EndP.Y+4, rad2degRS(pi/2-currentAngle))
	    else
	        Canvas.TextOut(EndP.X-4-Canvas.TextWidth(rad2degRS(currentAngle)), EndP.Y+3, rad2degRS(pi/2-currentAngle));
	        
		currentAngle := currentAngle + deg2rad(_longTickInterval); //increment currentAngle
	end;
	
	
end;

procedure TPendulumView._drawAnglePointer;
var
drawPoint: TPoint;
begin
    drawPoint.x := _origin.x+round(_tickDistance*cos(_angle));
    drawPoint.y := _origin.y+round(_tickDistance*sin(_angle));
    _drawArrowHead( drawPoint,_origin, drawPoint, _anglePointerLength, _anglePointerPenWidth, _anglePointerColor);
end;

procedure TPendulumView._drawAngleText(x, y: integer);
var
	outText: string;
begin
	_resetDrawingTools;
	outText := 'Angle: ' + formatFloat('000.##',rad2deg(Angle))+Chr(176);
	Canvas.TextOut(x,y,outText);
end;

function TPendulumView._getAngleTextSize : TSize;
var
	outText: string;
begin
	_resetDrawingTools;
	outText := 'Angle: ' + formatFloat('000.##',rad2deg(Angle))+Chr(176);
	_getAngleTextSize := Canvas.TextExtent(outText);
end;

procedure TPendulumView._setAngle(angle: real);
begin
        _angle := pi/2 - angle;
end;

function TPendulumView._getAngle : real;
begin
        _getAngle := pi/2 - _angle;
end;

end.




